<template>
  <div>
    <el-select
      v-model="selectId"
      :value-key="propValue"
      :multiple="multiple"
      :multiple-limit="multipleLimit"
      filterable
      clearable
      :placeholder="placeholder"
      :style="`width:${width}`"
      remote
      :remote-method="remoteMethod"
      :loading="loading"
      :readonly="readonly"
      :disabled="disabled"
      @change="emit('change')"
    >
      <el-option v-for="(item, index) in options" :key="index" :label="getLabel(item, propLabel)" :value="getValue(item, propValue)"></el-option>
    </el-select>
  </div>
</template>

<script setup lang="ts">
import urls from '@/api/request/urls';
import type { ResponseData } from '@/types/interface/base_interface';
import Request from '@/api/request/request';
import { onMounted, ref } from 'vue';
const emit = defineEmits(['change']);

interface Props {
  url: string; //服务端接口地址。和data一般是二选一，即如果是父组件已经传入了data则不不在从服务端读取数据
  queryName: string; //用于远程查询的名字,函数remoteMethod = async (query: string) 的名字。
  propValue?: string; //选项框中value的值的名称（选项中实际的值）
  propLabel?: string; //选项框中label的名称（选项中的标签，实际的值）。特别说明：
  propLabelRegexType?: number; //label的识别规则，即如何识别label中的标签。本组件的创新方式，可以供后续组件参考，具体规则参考getLabel函数的设计
  multiple?: boolean; //下拉选项是否可以多选
  multipleLimit?: number; //下拉框多选数量multiple 属性设置为 true 时，代表多选场景下用户最多可以选择的项目数， 为 0 则不限制
  placeholder?: string; //默认的提示内容
  width?: string; //下拉框宽度
  readonly?: boolean; //只读状态
  disabled?: boolean; //可用状态
  // defaultName?: string; // 新增,用于加载时就提供名字时使用
}
const props = withDefaults(defineProps<Props>(), {
  propValue: 'id', //默认是id名
  propLabel: 'name', //默认是name名
  labelRegexType: 2,
  multiple: false, //默认是单选
  multipleLimit: 0,
  placeholder: '',
  width: '200px',
  readonly: false,
  disabled: false
});

const selectId = defineModel<string>({
  required: false,
  default: ''
});

//定义下拉框的列表集合
const options = ref([]);
//是否正在加载数据
const loading = ref(false);
//远程加载数据
const remoteMethod = async (query: string) => {
  if (props.url) {
    loading.value = true;
    const resData: ResponseData = await new Request(props.url, null, {
      params: { [props.queryName]: query }
    }).get();
    loading.value = false;
    if (resData.success) {
      options.value = resData.data.data;
    }
  }
};
//因为label中的属性可能会要嵌套关联，例如显示item[material.name]，直接是无法显示的，必须通过调用这个方法。
const getValue = (item: any, name: string): string => {
  if (!name) return '';
  //使用 split('.') 方法将 name 按照点分隔符分割成一个数组 keys。例如，'material.name' 会被分割成 ['material', 'name']
  const keys = name.split('.');
  //初始化变量为item,在后续的循环中逐步深入对象的嵌套属性。
  let result: any = item;
  for (const key of keys) {
    if (result && typeof result === 'object' && key in result) {
      result = result[key];
    } else {
      return ''; // 如果在某个层级找不到属性，返回空字符串
    }
  }
  return String(result); // 确保返回的是字符串
};

const getLabel = (item: any, labelName: string) => {
  let result;
  if (props.propLabelRegexType == 1) {
    result = getLabel1(item, labelName);
  } else {
    result = getLabel2(item, labelName);
  }
  return result;
};
//===============================label识别方式1==========================================================
//核心思想是根据|分隔符来进行切割。
//设计想法：除了需要强制使用|来进行切割，在变量识别上，并不完全依赖于{}符号。
//简单的说，如果只有一个变量，建议使用该方法。否则建议使用方法2
// 传入的如果是多个值：'物料：{material.name}|批次号：{batchNumber}'
//如果是单个值：'物料：{material.name}。
//也可能是直接传变量的：material.name|batchNumber
//要求对将上面的变量的值替换成对应的变量值
const getLabel1 = (item: any, labelName: string) => {
  let result = ''; //最终要返回的变量值
  //如果包含|则分隔成多个变量
  if (labelName.includes('|')) {
    const names = labelName.split('|');
    for (let name of names) {
      result += getLabelName(item, name);
    }
  } else {
    //如果只传入了一个变量
    result = getLabelName(item, labelName);
  }
  return result;
};
//通过正则表达式来处理name，规则参考getLabel。本代码专门设计于支持getLabel函数，不单独使用。
const getLabelName = (item: any, name: string) => {
  const regex = '\\{(.*?)\\}'; //设计正则表达式
  // 使用正则表达式匹配 {} 中的内容
  const match = name.match(regex);
  if (match) {
    //如果匹配成功
    const key = match[1]; // 获取{}中的变量名
    let value = getValue(item, key); //获取变量名对应的值
    //通过getValue返回的null值在转换时的null为字符串
    if (value == undefined || value == null || value == 'null') {
      value = '无';
    }
    name = name.replace(`{${match[1]}}`, value); // 替换为对应的值
  } else {
    name = getValue(item, name);
  }
  return name;
};
//===============================label识别方式2==========================================================
//核心思想是完全根据{}符号来识别变量。
//设计想法：这种方式更容易被程序员理解，但是如果只是单个变量，也必须要加上{}，这种方式与本项目的其他组件使用方式完全不同。
//与getLabel不同，这里的分隔是完全按照{material.name}中的括号来判断数量，不根据|符号进行切割处理。
//传入的如果是多个值：'物料：{material.name}|批次号：{batchNumber}'
//如果是单个值：'物料：{material.name}。
//也可能是直接传变量的：{material.name}|{batchNumber}
//也可以直接传单个变量：{material.name}
//要求对将上面的变量的值替换成对应的变量值
const getLabel2 = (item: any, labelName: string) => {
  let result = labelName; //最终要返回的变量值
  //第一个 / 表示正则表达式的开始
  //二个 / 表示正则表达式的结束。
  // g 是正则表达式的标志，表示全局匹配（global），即匹配字符串中所有符合条件的子字符串，而不仅仅是第一个匹配项。
  //因为下面要使用exec则必须加上表达式的开头和结尾；如果使用类似name.match(regex)则不需要。
  const regex = /\{(.*?)\}/g;
  //用 while 循环和 regex.exec(str) 方法来逐个提取匹配项。exec 方法在每次调用时会返回下一个匹配项，直到没有更多匹配项时返回 null
  let match;
  while ((match = regex.exec(labelName)) !== null) {
    //match[0]为{material.name}，match[1]为material.name
    let value = getValue(item, match[1]); //获取变量名对应的值
    //通过getValue返回的null值在转换时的null为字符串
    if (value == undefined || value == null || value == 'null') {
      value = '无';
    }
    result = result.replace(match[0], value); // 替换为对应的值
  }
  return result;
};
onMounted(async () => {
  if (selectId.value) {
    //如果有默认值，就先搜索列表
    await remoteMethod('');
  }
});
</script>

<style scoped></style>
